<template>
  <div v-loading="loading" class="process-viewer">
    <el-header style="border-bottom: 1px solid rgb(218 218 218); height: auto">
      <div class="header-div">
        <div>
          <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
            <el-button size="small" icon="Rank" @click="processReZoom()" />
          </el-tooltip>
          <el-tooltip effect="dark" content="放大" placement="bottom">
            <el-button size="small" icon="ZoomIn" @click="processZoomIn()" />
          </el-tooltip>
          <el-tooltip effect="dark" content="缩小" placement="bottom">
            <el-button size="small" icon="ZoomOut" @click="processZoomOut()" />
          </el-tooltip>
        </div>
        <div>
          <div class="tips-label">
            <div class="un-complete">未完成</div>
            <div class="in-progress">进行中</div>
            <div class="complete">已完成</div>
          </div>
        </div>
      </div>
    </el-header>
    <div class="flow-containers">
      <el-container class="bpmn-el-container" style="align-items: stretch">
        <el-main style="padding: 0">
          <div class="canvas" ref="bpmnCanvas"></div>
        </el-main>
      </el-container>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick } from 'vue'
import BpmnViewer from 'bpmn-js'
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'
import type { ModuleDeclaration } from 'didi'
import { ElMessage } from 'element-plus'
import type Canvas from 'diagram-js/lib/core/Canvas'
import type { ModdleElement } from 'bpmn-moddle'
import EventBus from 'diagram-js/lib/core/EventBus'
import Overlays from 'diagram-js/lib/features/overlays/Overlays'
import { getBpmnModelView } from '@/api/flow/instance'
import type { TaskVo, InstanceVo } from '@/types/flow'
import { useDictOptions } from '@/utils/dict'
import { Close } from '@element-plus/icons-vue'

const loading = ref(false)
const bpmnCanvas = ref<HTMLElement>()
const bpmnViewer = ref<BpmnViewer>()

// 当前固定显示的悬浮框ID
const fixedOverlayId = ref<string | null>(null)

// 加载任务状态字典
const { options: taskStatusOptions } = useDictOptions('task_status')

// 获取状态标签
const getStatusLabel = (status: string) => {
  const option = taskStatusOptions.value.find((opt) => opt.value === status)
  return option?.label || status
}

const init = (id: string) => {
  loading.value = true
  nextTick(async () => {
    if (bpmnViewer.value) bpmnViewer.value.destroy()
    bpmnViewer.value = new BpmnViewer({
      container: bpmnCanvas.value,
      additionalModules: [
        {
          //禁止滚轮滚动
          zoomScroll: ['value', ''],
        },
        ZoomScrollModule,
        MoveCanvasModule,
      ] as ModuleDeclaration[],
    })
    //查询后端流程实例信息
    const res = await getBpmnModelView(id)
    bpmnViewer.value
      .importXML(res.data.bpmnXml)
      .then((result) => {
        const { warnings } = result
        console.log('success !', warnings)
        processReZoom()
        loading.value = false
        setProcessStatus(res.data)
        addEventBusListener()
      })
      .catch((err) => {
        const { warnings, message } = err
        console.log('something went wrong:', warnings, message)
        loading.value = false
        ElMessage.error('加载流程图失败')
      })
  })
}

const defaultZoom = ref(1) // 默认缩放比例

/** Zoom：恢复 */
const processReZoom = () => {
  defaultZoom.value = 1
  bpmnViewer.value?.get<Canvas>('canvas').zoom('fit-viewport', { x: 0, y: 0 })
}

/** Zoom：放大 */
const processZoomIn = (zoomStep = 0.1) => {
  const newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
  if (newZoom > 4) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
  }
  defaultZoom.value = newZoom
  bpmnViewer.value?.get<Canvas>('canvas').zoom(defaultZoom.value)
}

/** Zoom：缩小 */
const processZoomOut = (zoomStep = 0.1) => {
  const newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
  if (newZoom < 0.2) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
  }
  defaultZoom.value = newZoom
  bpmnViewer.value?.get<Canvas>('canvas').zoom(defaultZoom.value)
}

const processInstance = ref<InstanceVo>() // 流程实例
const tasks = ref<TaskVo[]>([]) // 流程任务

/** 高亮流程 */
const setProcessStatus = (view: any) => {
  // 设置相关变量
  if (!view || !view.processInstance) {
    return
  }
  processInstance.value = view.processInstance
  tasks.value = view.tasks
  if (loading.value || !bpmnViewer.value) {
    return
  }
  const { unfinishedTaskActivityIds, finishedTaskActivityIds, finishedSequenceFlowActivityIds } =
    view
  const canvas = bpmnViewer.value.get<Canvas>('canvas')
  // 已完成节点
  if (Array.isArray(finishedSequenceFlowActivityIds)) {
    finishedSequenceFlowActivityIds.forEach((item: string) => canvas.addMarker(item, 'highlight'))
  }
  if (Array.isArray(finishedTaskActivityIds)) {
    finishedTaskActivityIds.forEach((item: string) => canvas.addMarker(item, 'highlight'))
  }
  // 未完成节点
  if (Array.isArray(unfinishedTaskActivityIds)) {
    unfinishedTaskActivityIds.forEach((item: string) => canvas.addMarker(item, 'highlight-todo'))
  }
}

const addEventBusListener = () => {
  const eventBus = bpmnViewer.value?.get<EventBus>('eventBus')
  const overlays = bpmnViewer.value?.get<Overlays>('overlays')

  // 改为点击事件监听
  eventBus?.on<ModdleElement>('element.click', (e) => {
    // 如果点击的是已经固定显示的节点，则关闭悬浮框
    if (fixedOverlayId.value === e.element.id) {
      overlays?.clear()
      fixedOverlayId.value = null
      return
    }

    // 清除之前的悬浮框
    overlays?.clear()
    fixedOverlayId.value = null

    const matchedTasks = tasks.value.filter((t) => t.taskDefinitionKey === e.element.id)
    if (e.element.type === 'bpmn:UserTask' && matchedTasks.length > 0) {
      setTimeout(() => {
        // 直接以固定模式显示详情框
        genNodeDetailBox(e, overlays, matchedTasks, true)
      }, 10)
    }
  })

  // 添加点击事件监听，用于关闭固定的悬浮框
  document.addEventListener('click', (event) => {
    const target = event.target as HTMLElement
    // 如果点击了关闭按钮
    if (target.closest('.overlay-close-btn')) {
      const overlays = bpmnViewer.value?.get<Overlays>('overlays')
      overlays?.clear()
      fixedOverlayId.value = null
      // 阻止事件冒泡，避免触发其他点击事件
      event.stopPropagation()
    }
    // 如果点击了悬浮框外部区域且不是流程节点
    else if (
      fixedOverlayId.value !== null &&
      !target.closest('.verlays') &&
      !target.closest('.djs-element')
    ) {
      const overlays = bpmnViewer.value?.get<Overlays>('overlays')
      overlays?.clear()
      fixedOverlayId.value = null
    }
  })
}
const genNodeDetailBox = (e: any, overlays: Overlays, tasks: TaskVo[], isFixed = true) => {
  // 添加任务数量显示
  const taskCount = tasks.length > 1 ? `<div class="task-count">${tasks.length}个任务</div>` : ''

  // 添加关闭按钮
  const closeButton = `<div class="overlay-close-btn" title="关闭"><svg class="close-icon" viewBox="0 0 1024 1024"><path d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.12z" fill="currentColor"></path></svg></div>`

  const tasksHtml = tasks
    .map(
      (data, index) => `
    ${index > 0 ? '<div class="task-divider"></div>' : ''}
    <div class="task-item">
      <p><span class="label">审批人员:</span> <span class="value">${data.assignee || '无'}</span></p>
      <p><span class="label">节点状态:</span> <span class="value">${getStatusLabel(data.status)}</span></p>
      ${data.reason ? `<p><span class="label">审批意见:</span> <span class="value">${data.reason}</span></p>` : ''}
      <p><span class="label">开始时间:</span> <span class="value">${data.createTime || '无'}</span></p>
      <p><span class="label">结束时间:</span> <span class="value">${data.endTime || '无'}</span></p>
      <p><span class="label">审批耗时:</span> <span class="value">${data.durationInMillis || '无'}</span></p>
    </div>
  `,
    )
    .join('')

  // 计算最佳显示位置，避免在屏幕边缘被截断
  // 调整位置，使悬浮框显示在节点右侧
  const position = { top: -5, left: e.element.width + 10 }

  // 添加悬浮框
  overlays.add(e.element.id, {
    position: position,
    html: `<div class="verlays" style="opacity: 0">${closeButton}${taskCount}<div class="tasks-container">${tasksHtml}</div></div>`,
  })

  // 记录当前悬浮框ID
  fixedOverlayId.value = e.element.id

  // 添加淡入动画效果
  setTimeout(() => {
    const overlayElements = document.querySelectorAll('.verlays')
    if (overlayElements.length > 0) {
      const lastOverlay = overlayElements[overlayElements.length - 1]
      lastOverlay.style.opacity = '1'

      // 添加滚动事件监听，阻止事件冒泡到设计器
      const tasksContainer = lastOverlay.querySelector('.tasks-container')
      if (tasksContainer) {
        // 检查内容是否可滚动
        const checkScrollable = () => {
          if (tasksContainer.scrollHeight > tasksContainer.clientHeight) {
            tasksContainer.classList.add('can-scroll')
          } else {
            tasksContainer.classList.remove('can-scroll')
          }
        }

        // 初始检查
        checkScrollable()

        // 滚动事件处理
        tasksContainer.addEventListener(
          'wheel',
          (event) => {
            event.stopPropagation()
          },
          { passive: false },
        )

        // 触摸设备滚动事件处理
        tasksContainer.addEventListener(
          'touchmove',
          (event) => {
            event.stopPropagation()
          },
          { passive: false },
        )

        // 滚动位置变化时更新指示器状态
        tasksContainer.addEventListener('scroll', () => {
          // 如果滚动到底部，隐藏底部指示器
          if (
            tasksContainer.scrollHeight - tasksContainer.scrollTop <=
            tasksContainer.clientHeight + 5
          ) {
            tasksContainer.classList.remove('can-scroll')
          } else if (tasksContainer.scrollHeight > tasksContainer.clientHeight) {
            tasksContainer.classList.add('can-scroll')
          }
        })
      }
    }
  }, 10)
}

defineExpose({
  init,
})
</script>

<style lang="scss" scoped>
.header-div {
  display: flex;
  padding: 10px 0;
  justify-content: space-between;

  .tips-label {
    display: flex;
    div {
      margin-right: 10px;
      padding: 5px;
      font-size: 12px;
    }
    .un-complete {
      border: 1px solid #000;
    }
    .in-progress {
      background-color: rgb(255, 237, 204);
      border: 1px dashed orange;
    }
    .complete {
      background-color: rgb(204, 230, 204);
      border: 1px solid green;
    }
  }
}

.bpmn-el-container {
  height: calc(100vh - 350px);
}
.flow-containers {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  background: url('')
    repeat !important;

  .canvas {
    width: 100%;
    height: 100%;
  }
  .load {
    margin-right: 10px;
  }
  :deep(.el-form-item__label) {
    font-size: 13px;
  }

  :deep(.djs-palette) {
    left: 0 !important;
    top: 0;
    border-top: none;
  }

  :deep(.djs-container svg) {
    min-height: 650px;
  }

  :deep(.startEvent.djs-shape .djs-visual > :nth-child(1)) {
    fill: #77df6d !important;
  }
  :deep(.endEvent.djs-shape .djs-visual > :nth-child(1)) {
    fill: #ee7b77 !important;
  }
  :deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
    fill: green !important;
    stroke: green !important;
    fill-opacity: 0.2 !important;
  }
  :deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
    fill: green !important;
  }
  :deep(.highlight.djs-shape .djs-visual > path) {
    fill: green !important;
    fill-opacity: 0.2 !important;
    stroke: green !important;
  }
  :deep(.highlight.djs-connection > .djs-visual > path) {
    stroke: green !important;
  }

  // 边框滚动动画
  @keyframes path-animation {
    from {
      stroke-dashoffset: 100%;
    }

    to {
      stroke-dashoffset: 0%;
    }
  }

  :deep(.highlight-todo.djs-connection > .djs-visual > path) {
    animation: path-animation 60s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    stroke-dasharray: 4px !important;
    stroke: orange !important;
    fill-opacity: 0.2 !important;
    marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
  }

  :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
    animation: path-animation 60s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    stroke-dasharray: 4px !important;
    stroke: orange !important;
    fill: orange !important;
    fill-opacity: 0.2 !important;
  }
}
:deep(.verlays) {
  width: 300px;
  max-height: 400px; /* 设置最大高度 */
  background: rgb(102, 102, 102);
  border-radius: 4px;
  border: 2px solid var(--el-color-primary);
  color: #fff;
  padding: 15px 10px;
  cursor: default;
  box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.5);
  transition:
    opacity 0.3s ease-in-out,
    transform 0.2s ease;
  z-index: 1000; /* 确保悬浮框在最上层 */
  position: relative; /* 为关闭按钮定位做准备 */
  transform-origin: left center;
  animation: popup-animation 0.2s ease-out;

  @keyframes popup-animation {
    from {
      transform: scale(0.8);
      opacity: 0;
    }
    to {
      transform: scale(1);
      opacity: 1;
    }
  }

  /* 任务容器可以正常滚动 */
  .tasks-container {
    cursor: auto;
  }

  /* 关闭按钮样式 */
  .overlay-close-btn {
    position: absolute;
    top: 5px;
    right: 5px;
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 50%;
    cursor: pointer;
    z-index: 1001;
    transition: background-color 0.2s;

    &:hover {
      background-color: rgba(255, 255, 255, 0.4);
    }

    .close-icon {
      width: 14px;
      height: 14px;
      color: white;
    }
  }

  /* 任务数量显示样式 */
  .task-count {
    font-weight: bold;
    margin-bottom: 10px;
    padding-bottom: 5px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.3);
    text-align: center;
  }

  /* 添加滚动容器 */
  .tasks-container {
    max-height: 320px; /* 留出任务数量显示的空间 */
    overflow-y: auto;
    padding-right: 5px;
    overscroll-behavior: contain; /* 防止滚动传播 */
    -webkit-overflow-scrolling: touch; /* 在iOS上提供平滑滚动 */
    position: relative; /* 为滚动指示器定位 */

    /* 当内容可滚动时显示渐变指示器 */
    &::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      height: 20px;
      background: linear-gradient(to top, rgba(102, 102, 102, 0.8), transparent);
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.2s;
    }

    &.can-scroll::after {
      opacity: 1;
    }

    /* 滚动条样式 */
    &::-webkit-scrollbar {
      width: 6px;
    }

    &::-webkit-scrollbar-thumb {
      background-color: rgba(255, 255, 255, 0.3);
      border-radius: 3px;
    }

    &::-webkit-scrollbar-track {
      background-color: rgba(0, 0, 0, 0.1);
      border-radius: 3px;
    }
  }

  .task-item {
    p {
      line-height: 24px;
      margin: 0;
      padding: 0;
      display: flex;
      flex-wrap: wrap;

      .label {
        width: 70px;
        color: rgba(255, 255, 255, 0.8);
        flex-shrink: 0;
      }

      .value {
        flex: 1;
        word-break: break-all;
      }
    }
  }

  .task-divider {
    height: 1px;
    background-color: rgba(255, 255, 255, 0.2);
    margin: 10px 0;
  }
}
</style>
